<?php

/**
 * Description of ParentChild.
 *
 * Handles Records where there is a parent-child relationship in place.
 * e.g. pages and subpages, or categories and sub-categories, etc.
 * The $fields array must have an element $fields['parent'].
 *
 * @author Ian Clarence <ian.clarence@gmail.com>
 */
class ParentChild extends ActiveRecord {

    /**
     * The table name.
     * A dummy name given here, since this class does not correspond to a database table.
     * However, its subclasses do correspond to tables,
     * and in each case the table name will be replaced using late static binding.
     */
    const TABLE = 'parent_child';

    /**
     * The 'ancestry' is a list of ancestors stored in the database as a delimited string.
     * The ANCESTRY_DELIMITER is the character (usually a comma) delimiting the list.
     */
    const ANCESTRY_DELIMITER = ',';

    /**
     * This defines which field defines how the ancestors are listed, and is a database column name.
     * Defaults to storing as id numbers, but in subclasses, other fields can be used.
     */
    const ANCESTRY_FIELD_KEY = 'id';
    
    /**
     * The 'search' method needs to know whether it is numeric or not. For 'id' it will be.
     */
    const ANCESTRY_FIELD_KEY_NUMERIC = TRUE;

    /**
     * The parent record of this record.
     *
     * @var ParentChild
     */
    protected $parent;

    /**
     * Array of records: the children of this record.
     *
     * @var array
     */
    protected $children;

    /**
     * Array of records: the siblings of this record.
     *
     * @var array
     */
    protected $siblings;

    /**
     * Array of records: the ancestors of this record.
     *
     * @var array
     */
    protected $ancestors = array();

    /**
     * The earliest ancestor record of this record.
     *
     * @var ParentChild
     */
    protected $ancestor;

    /**
     * Array of records: the descendants of this record.
     *
     * @var array
     */
    protected $descendants = array();

    /**
     * The constructor.
     *
     * @param array $fields
     */
    public function __construct(array $fields = array()) {
        parent::__construct($fields);        
        if (isset($this->id)) {
            $this->fetchParent();
            $this->fetchChildren();
        }
    }

    /**
     * Fetching the parent record.
     */
    public function fetchParent() {
        $class = Utils::underscoreToCamelCase(static::TABLE, TRUE);
        $parent = $this->getParent();
        $searchField = $parent instanceof $class ? $parent->getId() : $parent;
        $this->parent = new $class;
        $this->parent->load($searchField);
    }

    /**
     * Fetching an array of child records.
     */
    public function fetchChildren() {
        $this->children = static::search($this->id, 'parent', TRUE, TRUE, $this);
    }

    /**
     * Getter for the children array.
     * 
     * @return array
     */
    public function getChildren() {
        return $this->children;
    }

    /**
     * Fetching an array of sibling records: i.e. those having the same parent as this one.
     * This record is included.
     */
    public function fetchSiblings() {
        $this->siblings = static::search($this->getParent(), 'parent', TRUE, TRUE, $this);
    }

    /**
     * Getter for the siblings array.
     *
     * @return array
     */
    public function getSiblings() {
        return $this->siblings;
    }

    /**
     * Parsing the ancestry string from the database and forming the ancestors.
     *
     * @return bool
     */
    private function parseAncestry() {
        if ($this->ancestry == NULL) {
            return FALSE;
        }
        $ancestors = explode(static::ANCESTRY_DELIMITER, $this->ancestry);
        $this->ancestors = array();
        foreach ($ancestors as $ancestorKey => $ancestor) {            
            $values = static::search(
                $ancestor,
                static::ANCESTRY_FIELD_KEY,
                static::ANCESTRY_FIELD_KEY_NUMERIC,
                TRUE,
                $this
            );
            if (is_array($values)) {
                if (isset($values[0])) {
                    $this->ancestors[$ancestorKey] = $values[0];
                }
            }
        }
        return TRUE;
    }

    /**
     * Building the ancestry string from the array of ancestors, and store in the database.
     */
    private function buildAncestry() {
        $ancestryFieldKey = static::ANCESTRY_FIELD_KEY;
        $ancestors = array();
        foreach ($this->ancestors as $ancestorKey => $ancestor) {
            if ($ancestor->$ancestryFieldKey != NULL) {
                $ancestors[$ancestorKey] = $ancestor->$ancestryFieldKey;
            }
        }
        $this->ancestry = implode(static::ANCESTRY_DELIMITER, $ancestors);
        if ($this->ancestry == null) {
            return;
        }
        $this->save();
    }

    /**
     * Recursively finds the ancestors of this record as an array of records, and
     * store ancestry as a delimited string in the database.
     * If there is already an ancestry string then parse it to find the ancestors.
     * This record is the last element of the array.
     * It may be necessary to force rebuilding of the ancestry sting in the database.
     * This is done by setting $rebuildAncestry to TRUE.
     *
     * @param bool $rebuildAncestry
     */
    public function fetchAncestors($rebuildAncestry = FALSE) {
        /**
         * First see if there is an ancestry string in the database.
         */
        if ($this->parseAncestry() && !$rebuildAncestry) {
            return;
        }
        /**
         * Class name from table name.
         */
        $class = Utils::underscoreToCamelCase(static::TABLE, TRUE);
        /**
         * Creating a copy of this record as an instance of the appropriate class.
         */
        $me = new $class();
        $me->load($this->id);
        /**
         * Get parent's ancestors, and add them to the current page.
         */
        $parent = $this->getParent();
        if ($parent instanceof $class) {
            $parent->load($parent->getId());
            $parent->fetchAncestors();
            $this->ancestors = $parent->getAncestors();
            array_push($this->ancestors, $me);
        }
        /**
         * If there is no parent, just use the current page.
         */
        else {
            $this->ancestors = array($me);
        }
        /**
         * Build the ancestry for the database.
         */
        $this->buildAncestry();
    }

    /**
     * Getter for the ancestors array.
     *
     * @return array
     */
    public function getAncestors() {
        return $this->ancestors;
    }

    /**
     * Fetching a page's earliest ancestor.
     */
    public function fetchAncestor() {
        $this->ancestor = $this->ancestors[0];
    }

    /**
     * Getter for the page's earliest ancestor.
     *
     * @return ParentChild
     */
    public function getAncestor() {
        return $this->ancestor;
    }
    
    /**
     * Returning the array of records with no parent records.
     * This does not depend upon any particular record, so does not have to be instantiated.
     * Therefore is static.
     * 
     * @param bool $cascade
     * @return class 
     */
    public function getPrimaries($cascade = FALSE) {
        $apcKey = static::TABLE . 'primaries' . static::apcKeySuffix($this);
        $params = array('table' => static::TABLE, 'search_where' => static::searchWhere($this));
        $callback = function($params) {
            $query = new Query('
                SELECT *
                FROM :table
                WHERE `parent` is NULL :searchWhere;
            ');
            $query->bindFieldName('table', $params['table']);
            $query->bindTextString('searchWhere', $params['search_where']);
            return System::getDB()->queryArray($query);
        };        
        $values = Utils::apcCache($apcKey, $callback, STORED_AS_CACHE == YES, $params);
        
        $class = Utils::underscoreToCamelCase(static::TABLE, TRUE);
        $records = array();
        if (is_array($values)) {
            foreach ($values as $valueKey => $value) {
                $records[$valueKey] = new $class();
                if ($cascade) {
                    $records[$valueKey]->load($value['id']);
                }
                else {
                    $records[$valueKey]->setId($value['id']);
                }
            }
        }
        return $records;
    }

    /**
     * Recursively finds the descendants of this record as an array of records.
     * If any page gets modified, then the ancestry strings of its descendant pages may become incorrect,
     * in which case it will be necessary to rebuild the ancestry string of each descendant page.
     * This is done by setting $rebuildAncestry to TRUE.
     *
     * @param bool $rebuildAncestry
     */
    public function fetchDescendants($rebuildAncestry = FALSE) {
        $class = Utils::underscoreToCamelCase(static::TABLE, TRUE);
        $this->fetchChildren();
        $this->children = $this->getChildren();
        $this->descendants = $this->children;
        if (is_array($this->children)) {
            foreach ($this->children as $child) {
                if ($child instanceof $class) {
                    $child->loadAll();
                    $child->fetchAncestors($rebuildAncestry);
                    $child->fetchDescendants($rebuildAncestry);
                    $this->descendants = array_merge($this->descendants, $child->getDescendants());
                }
            }
        }
    }

    /**
     * Getter for the descendants array.
     *
     * @return array
     */
    public function getDescendants() {
        return $this->descendants;
    }

    /**
     * Getting page parameters for passing into a view.
     * These are the existing page fields plus extra data.
     *
     * @return array
     */
    public function getViewFields() {        

        /**
         * Parent.
         */
        $this->fetchParent();
        $parent = $this->getParent();
        $class = get_called_class();
        if ($parent instanceof $class) {
            $viewFields['parent'] = $parent->getFields();
        }

        /**
         * Children.
         */
        $this->fetchChildren();
        $children = $this->getChildren();
        $viewFields['children'] = self::getFieldsFromRecords($children);

        /**
         * Siblings.
         */
        $this->fetchSiblings();
        $siblings = $this->getSiblings();
        $viewFields['siblings'] = self::getFieldsFromRecords($siblings);

        /**
         * Primaries.
         */
        $primaries = $this->getPrimaries(TRUE);
        $viewFields['primaries'] = self::getFieldsFromRecords($primaries);

        /**
         * Descendents.
         */
        $this->fetchDescendants();
        $descendants = $this->getDescendants();
        $viewFields['descendants'] = self::getFieldsFromRecords($descendants);

        /**
         * Ancestors.
         */
        $this->fetchAncestors();
        $ancestors = $this->getAncestors();
        $viewFields['ancestors'] = self::getFieldsFromRecords($ancestors);
        
        return $viewFields;

    }

}